package com.boarsoft.boar.eureka;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.boarsoft.common.Util;
import com.boarsoft.common.util.FileUtil;
import com.boarsoft.common.util.HttpClientUtil;
import com.boarsoft.common.util.InetUtil;
import com.boarsoft.common.util.StreamUtil;
import com.boarsoft.soagov.bean.AppInstReg;
import com.boarsoft.soagov.registry.ServiceRegistry;

public class EurekaRegistry implements ServiceRegistry, Runnable {
	private static final Logger log = LoggerFactory.getLogger(EurekaRegistry.class);

	protected String serverUrl = "http://localhost:8761/eureka";
	protected String appId;
	protected String appGroup;
	protected int port;
	protected int interval = 30;
	protected int duration = 90;
	protected Map<String, String> meta = new HashMap<>();
	protected String instanceId;

	/** 实例IP:PORT */
	protected String addr;
	/** k: appGroup/appId, v: providers */
	protected Map<String, List<AppInstReg>> data = new ConcurrentHashMap<>();
	// http://localhost:8761/eureka/apps/{appId}/{instanceId}:{port}
	protected String heartBeatUrl;
	protected String queryUrl;
	protected ScheduledExecutorService scheduler;
	protected Future<?> future;

	@PostConstruct
	public void init() {
		StringBuilder sb = new StringBuilder();
		// 实例IP:PORT
		sb.setLength(0);
		this.addr = InetUtil.getAddr();
		if (Util.strIsEmpty(instanceId)) {
			this.instanceId = addr;
		}
		sb.setLength(0);
		// 查询地址
		queryUrl = sb.append(serverUrl).append("/apps").toString();
		// 心跳地址
		heartBeatUrl = sb.append("/").append(appId)//
				.append("/").append(instanceId).toString();
		this.register();// 注册
		this.query();// 先查一次
		// 开始心跳
		if (interval > 0) {
			future = scheduler.scheduleAtFixedRate(this, interval, interval, TimeUnit.SECONDS);
		}
	}

	@PreDestroy
	public void destroy() {
		if (future != null) {
			future.cancel(true);
		}
		this.deregister();
	}

	@Override
	public void run() {
		try {
			if (this.heartbeat()) {
				this.query();
			}
			Thread.sleep(interval * 1000);
		} catch (Throwable e) {
			log.error("Error on do heart beat to with url {}", heartBeatUrl, e);
		}
	}

	public boolean query() {
		String xml = HttpClientUtil.sendGet(queryUrl);
		log.debug("Query eureka server with {} return {}", queryUrl, xml);
		if (Util.strIsNotEmpty(xml)) {
			try {
				this.process(xml);
				return true;
			} catch (DocumentException e) {
				log.error("Error on query eureka app instances with {}", queryUrl);
			}
		}
		return false;
	}

	public boolean heartbeat() {
		log.debug("Heart beat to eureka server with url {}", heartBeatUrl);
		StringBuilder sb = new StringBuilder();
		PrintWriter out = null;
		BufferedReader br = null;
		HttpURLConnection conn = null;
		try {
			conn = (HttpURLConnection) new URL(heartBeatUrl).openConnection();
			conn.setConnectTimeout(3000);
			conn.setReadTimeout(30000);
			conn.setRequestProperty("User-Agent", "Mozilla/5.0");
			conn.setRequestProperty("Content-Type", "application/xml");
			// conn.setDoOutput(true);
			// conn.setDoInput(true);
			conn.setRequestMethod("PUT");
			// out = new PrintWriter(conn.getOutputStream());
			// out.print(xml);
			// out.flush();
			sb.setLength(0);
			br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
			String line = null;
			while ((line = br.readLine()) != null) {
				sb.append(line).append(FileUtil.LINE_SEPARATOR);
			}
			if (conn.getResponseCode() == 200) {
				return true;
			}
			if (conn.getResponseCode() == 404) {
				// 尝试重新注册
				this.register();
			}
			log.error("Heart beat to eureka server {} failed, response = {}", heartBeatUrl, sb);
		} catch (FileNotFoundException e) {
			this.register();
			return false;
		} catch (Throwable e) {
			log.error("Error on heart beat to eureka server {}", heartBeatUrl, e);
		} finally {
			StreamUtil.close(br);
			StreamUtil.close(out);
			if (conn != null) {
				conn.disconnect();
			}
		}
		return false;
	}

	public boolean register() {
		// http://localhost:8761/eureka/apps/{appId}
		StringBuilder sb = new StringBuilder();
		String url = sb.append(serverUrl).append("/apps/").append(appId).toString();
		String xml = this.getRegisterXml();
		PrintWriter out = null;
		BufferedReader br = null;
		HttpURLConnection conn = null;
		try {
			conn = (HttpURLConnection) new URL(url).openConnection();
			conn.setConnectTimeout(3000);
			conn.setReadTimeout(30000);
			conn.setRequestProperty("User-Agent", "Mozilla/5.0");
			conn.setRequestProperty("Content-Type", "application/xml");
			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setRequestMethod("POST");
			// 设置cookie
			// if (cookie != null) {
			// conn.setRequestProperty("Cookie", cookie);
			// }
			// headers
			// if (headers != null && headers.size() > 0) {
			// for (String h : headers.keySet())
			// conn.setRequestProperty(h, headers.get(h));
			// }
			// body
			out = new PrintWriter(conn.getOutputStream());
			out.print(xml);
			out.flush();
			sb.setLength(0);
			br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
			String line = null;
			while ((line = br.readLine()) != null) {
				sb.append(line).append(FileUtil.LINE_SEPARATOR);
			}
			if (conn.getResponseCode() == 204) {
				return true;
			}
			log.error("Register to eureka server {} failed, response = {}", url, sb);
		} catch (Throwable e) {
			log.error("Error on register to eureka server {}", url, e);
		} finally {
			StreamUtil.close(br);
			StreamUtil.close(out);
			if (conn != null) {
				conn.disconnect();
			}
		}
		return false;
	}

	public boolean deregister() {
		// http://localhost:8761/eureka/apps/{appId}/{instanceId}:{port}
		String url = new StringBuilder().append(serverUrl)//
				.append(appId).append("/").append(instanceId).toString();
		log.info("Deregister app instance with url {}", url);
		StringBuilder sb = new StringBuilder();
		PrintWriter out = null;
		BufferedReader br = null;
		HttpURLConnection conn = null;
		try {
			conn = (HttpURLConnection) new URL(url).openConnection();
			conn.setConnectTimeout(3000);
			conn.setReadTimeout(30000);
			conn.setRequestProperty("User-Agent", "Mozilla/5.0");
			conn.setRequestProperty("Content-Type", "application/xml");
			// conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setRequestMethod("DELETE");
			// 设置cookie
			// if (cookie != null) {
			// conn.setRequestProperty("Cookie", cookie);
			// }
			// headers
			// if (headers != null && headers.size() > 0) {
			// for (String h : headers.keySet())
			// conn.setRequestProperty(h, headers.get(h));
			// }
			// body
			// out = new PrintWriter(conn.getOutputStream());
			// out.print(xml);
			// out.flush();
			sb.setLength(0);
			br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
			String line = null;
			while ((line = br.readLine()) != null) {
				sb.append(line).append(FileUtil.LINE_SEPARATOR);
			}
			if (conn.getResponseCode() == 200) {
				return true;
			}
			log.error("Deregister to eureka server {} failed, response = {}", url, sb);
		} catch (Throwable e) {
			log.error("Error on deregister to eureka server {}", url, e);
		} finally {
			StreamUtil.close(br);
			StreamUtil.close(out);
			if (conn != null) {
				conn.disconnect();
			}
		}
		return false;
	}

	@SuppressWarnings("unchecked")
	protected void process(String xml) throws DocumentException {
		Map<String, List<AppInstReg>> map = new HashMap<>();
		StringBuilder sb = new StringBuilder();
		Document doc = new SAXReader().read(new StringReader(xml));
		List<Node> aLt = doc.selectNodes("/applications/application");
		String sk = null;
		String addr = null;
		for (Node a : aLt) {
			String appId = a.selectSingleNode("name").getStringValue();
			List<Node> iLt = a.selectNodes("instance");
			for (Node i : iLt) {
				String status = i.selectSingleNode("status").getStringValue();
				if (!"UP".equals(status)) {
					continue;
				}
				String id = this.getStringValue(i, "instanceId");
				String ip = this.getStringValue(i, "ipAddr");
				String port = this.getStringValue(i, "port");
				if (Util.strIsEmpty(port)) {
					log.warn("Port of {}/{} is missing", appId, id);
					continue;
				}
				String group = this.getStringValue(i, "appGroupName");
				if (Util.strIsEmpty(group)) {
					// log.warn("Group of {}/{} is missing", appId, id);
					continue;
				}
				// Node m = i.selectSingleNode("metadata");
				// String version =
				// m.selectSingleNode("version").getStringValue();
				sb.setLength(0);
				sk = sb.append(group).append("/").append(appId).toString();
				// sb.append("/").append("version");
				List<AppInstReg> pLt = map.get(sk);
				if (pLt == null) {
					pLt = new ArrayList<AppInstReg>();
					map.put(sk, pLt);
				}
				sb.setLength(0);
				addr = sb.append(ip).append(":").append(port).toString();
				AppInstReg reg = new AppInstReg();
				reg.setAddr(addr);
				// reg.setHandler(handler);
				// reg.setPath(path);
				pLt.add(reg);
				log.debug("Add instance {} of service {}", addr, sk);
			}
		}
		if (!map.isEmpty()) {
			data.putAll(map);
		}
	}

	protected String getStringValue(Node n, String path) {
		n = n.selectSingleNode(path);
		if (n == null) {
			return null;
		}
		return n.getStringValue();
	}

	@Override
	public List<String> getProviders(String env, String sk) {
		List<AppInstReg> oLt = data.get(sk);
		List<String> rLt = new ArrayList<>();
		for (AppInstReg o : oLt) {
			rLt.add(o.getAddr());
		}
		return rLt;
	}

	protected String getRegisterXml() {
		StringBuilder sb = new StringBuilder();
		String ip = InetUtil.getIp();
		//
		sb.setLength(0);
		sb.append("<instance>");
		sb.append("  <instanceId>").append(instanceId).append("</instanceId>");
		sb.append("  <hostName>").append(InetUtil.getHostName()).append("</hostName>");
		sb.append("  <app>").append(appId).append("</app>");
		sb.append("  <appGroupName>").append(appGroup).append("</appGroupName>");
		sb.append("  <ipAddr>").append(ip).append("</ipAddr>");
		sb.append("  <vipAddress>").append(appId).append("</vipAddress>");
		sb.append("  <secureVipAddress>").append(ip).append("</secureVipAddress>");
		sb.append("  <status>UP</status>");
		sb.append("  <port enabled=\"true\">").append(port).append("</port>");
		sb.append("  <securePort enabled=\"false\">443</securePort>");
		sb.append("  <homePageUrl>http://").append(ip).append(":").append(port).append("/</homePageUrl>");
		sb.append("  <statusPageUrl>http://").append(addr).append("/actuator/info</statusPageUrl>");
		sb.append("  <healthCheckUrl>http://").append(addr).append("/actuator/health</healthCheckUrl>");
		sb.append("  <dataCenterInfo class=\"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo\">");
		sb.append("    <name>MyOwn</name>");
		sb.append("  </dataCenterInfo>");
		sb.append("  <leaseInfo>");
		// sb.append("
		// <evictionDurationInSecs>").append(duration).append("</evictionDurationInSecs>");
		sb.append("    <renewalIntervalInSecs>").append(interval).append("</renewalIntervalInSecs>");
		sb.append("    <durationInSecs>").append(duration).append("</durationInSecs>");
		// sb.append("<registrationTimestamp>1605081538070</registrationTimestamp>");
		// sb.append("<lastRenewalTimestamp>1607502615251</lastRenewalTimestamp>");
		// sb.append(" <evictionTimestamp>0</evictionTimestamp>");
		// sb.append(" <serviceUpTimestamp>1605080830603</serviceUpTimestamp>");
		sb.append("  </leaseInfo>");
		if (meta != null && !meta.isEmpty()) {
			sb.append("  <metadata>");
			for (String k : meta.keySet()) {
				sb.append("    <").append(k).append(">")//
						.append(meta.get(k)).append("</").append(k).append(">");
			}
			sb.append("  </metadata>");
		}
		sb.append("  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>");
		// sb.append("
		// <lastUpdatedTimestamp>1605081538070</lastUpdatedTimestamp>");
		// sb.append(" <lastDirtyTimestamp>1605081542583</lastDirtyTimestamp>");
		// sb.append(" <actionType>ADDED</actionType>");
		sb.append("</instance>");
		return sb.toString();
	}

	public void setInterval(int interval) {
		if (interval < 1) {
			interval = Integer.MAX_VALUE;
		}
		this.interval = interval;
	}

	public void setDuration(int duration) {
		if (duration < 1) {
			duration = Integer.MAX_VALUE;
		}
		this.duration = duration;
	}

	@Override
	public List<AppInstReg> list(String ip) {
		List<AppInstReg> lt = new ArrayList<>();
		String s = ip.concat(":");
		for (List<AppInstReg> aLt : data.values()) {
			for (AppInstReg a : aLt) {
				if (a.getAddr().startsWith(s)) {
					lt.add(a);
				}
			}
		}
		return lt;
	}

	@Override
	public AppInstReg find(String ip, int port) {
		String s = new StringBuilder().append(ip)//
				.append(":").append(port).toString();
		for (List<AppInstReg> aLt : data.values()) {
			for (AppInstReg a : aLt) {
				if (a.getAddr().equals(s)) {
					return a;
				}
			}
		}
		return null;
	}

	@Override
	public String getHandler(String appId) {
		//TODO
		return null;
	}

	// ---------------------------
	public String getServerUrl() {
		return serverUrl;
	}

	public void setServerUrl(String serverUrl) {
		this.serverUrl = serverUrl;
	}

	public String getAppId() {
		return appId;
	}

	public void setAppId(String appId) {
		this.appId = appId;
	}

	public String getAppGroup() {
		return appGroup;
	}

	public void setAppGroup(String appGroup) {
		this.appGroup = appGroup;
	}

	public String getInstanceId() {
		return instanceId;
	}

	public void setInstanceId(String instanceId) {
		this.instanceId = instanceId;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public int getInterval() {
		return interval;
	}

	public int getDuration() {
		return duration;
	}

	public Map<String, String> getMeta() {
		return meta;
	}

	public void setMeta(Map<String, String> meta) {
		this.meta = meta;
	}

	public String getHeartBeatUrl() {
		return heartBeatUrl;
	}

	public void setHeartBeatUrl(String heartBeatUrl) {
		this.heartBeatUrl = heartBeatUrl;
	}

	public ScheduledExecutorService getScheduler() {
		return scheduler;
	}

	public void setScheduler(ScheduledExecutorService scheduler) {
		this.scheduler = scheduler;
	}

	public Future<?> getFuture() {
		return future;
	}

	public void setFuture(Future<?> future) {
		this.future = future;
	}
}